/**
 * SimpleRAG Frontend Logic
 * Modernized with real streaming, markdown rendering, and improved UX.
 */

// State
const state = {
  mode: 'rag', // 'rag' | 'agent'
  streaming: false,
  controller: null,
  kbId: 'default',
  kbList: [],
};

// Elements
const dom = {
  chatHistory: document.getElementById('chat-history'),
  chatInput: document.getElementById('chat-input'),
  sendBtn: document.getElementById('send-btn'),
  clearBtn: document.getElementById('clear-chat'),
  showSources: document.getElementById('show-sources'),
  configForm: document.getElementById('config-form'),
  fileInput: document.getElementById('file-input'),
  dropZone: document.getElementById('drop-zone'),
  uploadStatus: document.getElementById('upload-status'),
  rebuildBtn: document.getElementById('rebuild-index-btn'),
  saveConfigBtn: document.getElementById('save-config-btn'),
  tabs: document.querySelectorAll('.nav-item'),
  views: document.querySelectorAll('.view'),
  modeBtns: document.querySelectorAll('.mode-btn'),
  kbSelect: document.getElementById('kb-select'),
  refreshKb: document.getElementById('refresh-kb'),
  kbSelectFiles: document.getElementById('kb-select-files'),
  kbDropdown: document.getElementById('kb-dropdown'),
  kbTrigger: document.getElementById('kb-trigger'),
  kbMenu: document.getElementById('kb-menu'),
  currentKbName: document.getElementById('current-kb-name'),
  kbCreateBtn: document.getElementById('kb-create-btn'),
  kbName: document.getElementById('kb-name'),
  kbRefreshBtn: document.getElementById('kb-refresh-btn'),
  kbList: document.getElementById('kb-list'),
  fileList: document.getElementById('file-list'),
  updateIndexBtn: document.getElementById('update-index-btn'),
  deleteIndexBtn: document.getElementById('delete-index-btn'),
  kbMigrateBtn: document.getElementById('kb-migrate-btn'),
  toast: document.getElementById('toast-container'),
};

// --- Toast helper ---
function showToast(type, message) {
  if (!dom.toast) return alert(message);
  const toast = document.createElement('div');
  toast.className = `toast ${type}`;
  toast.innerText = message;
  dom.toast.appendChild(toast);
  setTimeout(() => {
    toast.remove();
  }, 3200);
}

// --- Initialization ---
document.addEventListener('DOMContentLoaded', () => {
  initTabs();
  initKB();
  initConfig();
  initChat();
  initUpload();
  
  // Marked config
  if (window.marked) {
    window.marked.setOptions({
      highlight: function(code, lang) {
        if (lang && hljs.getLanguage(lang)) {
          return hljs.highlight(code, { language: lang }).value;
        }
        return hljs.highlightAuto(code).value;
      },
      breaks: true
    });
  }
});

// --- Navigation ---
function initTabs() {
  dom.tabs.forEach(tab => {
    tab.addEventListener('click', () => {
      // Active Tab
      dom.tabs.forEach(t => t.classList.remove('active'));
      tab.classList.add('active');
      
      // Active View
      const targetId = `view-${tab.dataset.tab}`;
      dom.views.forEach(v => {
        v.classList.remove('active');
        if (v.id === targetId) v.classList.add('active');
      });
    });
  });

  dom.modeBtns.forEach(btn => {
    btn.addEventListener('click', () => {
      dom.modeBtns.forEach(b => b.classList.remove('active'));
      btn.classList.add('active');
      state.mode = btn.dataset.mode;
      addSystemMessage(`已切换到 ${btn.innerText}`);
    });
  });
}

// --- KB Management ---
async function initKB() {
  await loadKBList();

  // Dropdown logic
  if (dom.kbTrigger) {
    dom.kbTrigger.addEventListener('click', (e) => {
      e.stopPropagation();
      dom.kbDropdown.classList.toggle('open');
    });
  }
  document.addEventListener('click', (e) => {
    if (dom.kbDropdown && !dom.kbDropdown.contains(e.target)) {
      dom.kbDropdown.classList.remove('open');
    }
  });

  if (dom.kbSelect) {
    dom.kbSelect.addEventListener('change', (e) => setKB(e.target.value));
  }
  if (dom.kbSelectFiles) {
    dom.kbSelectFiles.addEventListener('change', (e) => setKB(e.target.value));
  }
  if (dom.refreshKb) {
    dom.refreshKb.addEventListener('click', () => loadKBList(true));
  }
  if (dom.kbRefreshBtn) {
    dom.kbRefreshBtn.addEventListener('click', () => loadKBList(true));
  }
  if (dom.kbCreateBtn) {
    dom.kbCreateBtn.addEventListener('click', async () => {
      const name = (dom.kbName.value || '').trim();
      if (!name) return showToast('warn', '请输入知识库名称');
      try {
        const res = await fetch('/kb', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ name }),
        });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const data = await res.json();
        dom.kbName.value = '';
        await loadKBList(true, data.kb.id);
        showToast('success', '知识库创建成功');
      } catch (e) {
        showToast('danger', '创建失败: ' + e.message);
      }
    });
  }
  if (dom.updateIndexBtn) {
    dom.updateIndexBtn.addEventListener('click', async () => {
      await triggerUpdateIndex(state.kbId);
    });
  }
  if (dom.deleteIndexBtn) {
    dom.deleteIndexBtn.addEventListener('click', async () => {
      if (state.kbId === 'default') return showToast('warn', '默认知识库不支持删除');
      if (!confirm('确认删除该知识库？索引将被移除，可选是否删除文件。')) return;
      const deleteFiles = confirm('是否连同源文件一起删除？');
      try {
        const res = await fetch(`/kb/${state.kbId}`, {
          method: 'DELETE',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ delete_files: deleteFiles }),
        });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        await loadKBList(true, 'default');
        showToast('success', '知识库已删除');
      } catch (e) {
        showToast('danger', '删除失败: ' + e.message);
      }
    });
  }
  if (dom.kbMigrateBtn) {
    dom.kbMigrateBtn.addEventListener('click', async () => {
      if (!confirm('将默认数据/索引复制到新知识库 main，继续？')) return;
      try {
        dom.kbMigrateBtn.disabled = true;
        dom.kbMigrateBtn.innerText = '迁移中...';
        const res = await fetch('/kb/migrate_default', { method: 'POST' });
        const data = await res.json();
        if (!res.ok || data.success === false) throw new Error(data.detail || data.message || '迁移失败');
        showToast('success', `迁移完成，KB: ${data.kb_id}`);
        await loadKBList(true, data.kb_id || 'main');
      } catch (e) {
        showToast('danger', '迁移失败: ' + e.message);
      } finally {
        dom.kbMigrateBtn.disabled = false;
        dom.kbMigrateBtn.innerText = '迁移默认数据';
      }
    });
  }
}

async function loadKBList(force = false, preferId = null) {
  try {
    const res = await fetch('/kb');
    const data = await res.json();
    state.kbList = data.items || [];
    const hasCurrent = state.kbList.find((k) => k.id === state.kbId);
    if (!hasCurrent) {
      state.kbId = preferId || (state.kbList[0]?.id || 'default');
    } else if (preferId) {
      state.kbId = preferId;
    }
    renderKBSelects();
    renderKBList();
    await loadFilesForKB(state.kbId);
  } catch (e) {
    console.error('加载知识库失败', e);
  }
}

function renderKBSelects() {
  const options = state.kbList
    .map((kb) => `<option value="${kb.id}" ${kb.id === state.kbId ? 'selected' : ''}>${kb.name || kb.id}</option>`)
    .join('');
  
  // Update native select (files view)
  if (dom.kbSelectFiles) dom.kbSelectFiles.innerHTML = options;
  if (dom.kbSelect) dom.kbSelect.innerHTML = options; // Keep for fallback or hidden use

  // Update custom dropdown (chat view)
  if (dom.kbMenu) {
    dom.kbMenu.innerHTML = state.kbList.map(kb => {
        const stats = kb.stats || {};
        const fileCount = stats.file_stats?.total_files ?? 0;
        const activeClass = kb.id === state.kbId ? 'active' : '';
        return `
            <div class="dropdown-item ${activeClass}" onclick="setKB('${kb.id}')">
                <div class="kb-info-mini">
                    <span class="kb-name-mini">${kb.name || kb.id}</span>
                    <span class="kb-meta-mini">${fileCount} 文件</span>
                </div>
                ${activeClass ? '<svg viewBox="0 0 24 24" width="16" stroke="currentColor" fill="none" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>' : ''}
            </div>
        `;
    }).join('');
    
    // Update trigger text
    const current = state.kbList.find(k => k.id === state.kbId);
    if (dom.currentKbName && current) {
        dom.currentKbName.innerText = current.name || current.id;
    }
  }
}

function renderKBList() {
  if (!dom.kbList) return;
  dom.kbList.innerHTML = state.kbList
    .map((kb) => {
      const stats = kb.stats || {};
      const indexExists = stats.index_info?.index_exists || false;
      const fileCount = stats.file_stats?.total_files ?? '-';
      const updated = kb.updated_at ? kb.updated_at.split('T')[0] : 'N/A';
      const active = kb.id === state.kbId ? 'active' : '';
      return `
        <div class="kb-card ${active}">
          <div class="kb-card-header">
            <div>
              <div class="kb-name">${kb.name || kb.id}</div>
              <div class="kb-id">ID: ${kb.id}</div>
            </div>
            <span class="kb-badge ${indexExists ? 'ok' : 'warn'}">${indexExists ? '已索引' : '未索引'}</span>
          </div>
          <div class="kb-meta">
            <span>文件: ${fileCount}</span>
            <span>更新: ${updated}</span>
          </div>
          <div class="kb-paths">
            <div>data: ${kb.data_dir}</div>
            <div>index: ${kb.index_dir}</div>
          </div>
          <div class="kb-actions">
            <button class="action-btn small" onclick="setKB('${kb.id}')">切换</button>
          </div>
        </div>
      `;
    })
    .join('');
}

async function setKB(kbId) {
  state.kbId = kbId;
  renderKBSelects();
  renderKBList();
  await loadFilesForKB(kbId);
  addSystemMessage(`已切换到知识库：${kbId}`);
}

async function loadFilesForKB(kbId) {
  if (!dom.fileList) return;
  try {
    const res = await fetch(`/kb/${kbId}/files`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const data = await res.json();
    renderFileList(data.files || []);
  } catch (e) {
    dom.fileList.innerHTML = `<div class="file-empty">加载文件失败：${e.message}</div>`;
  }
}

function getFileIcon(fileType, ext) {
  const t = (fileType || ext || '').toLowerCase();
  if (t.includes('pdf')) return '📕';
  if (t.includes('word') || t.includes('.doc')) return '📄';
  if (t.includes('excel') || t.includes('.xls')) return '📊';
  if (t.includes('markdown') || t.includes('.md')) return '📘';
  if (t.includes('html')) return '🌐';
  if (t.includes('csv')) return '🧾';
  if (t.includes('ppt')) return '📑';
  return '📁';
}

function renderFileList(files) {
  if (!dom.fileList) return;
  if (!files.length) {
    dom.fileList.innerHTML = `<div class="file-empty">当前知识库暂无文件</div>`;
    return;
  }
  dom.fileList.innerHTML = files
    .map((f) => {
      const icon = getFileIcon(f.file_type, f.extension);
      const sizeMB = f.size ? (f.size / (1024 * 1024)).toFixed(2) : '-';
      return `
        <div class="file-row">
          <div class="file-main">
            <span class="file-icon">${icon}</span>
            <div>
              <div class="file-name">${f.name || f.path.split('/').pop()}</div>
              <div class="file-meta">${f.file_type || f.extension || '未知类型'} · ${sizeMB} MB</div>
            </div>
          </div>
          <div class="file-path" title="${f.path}">${f.path}</div>
        </div>
      `;
    })
    .join('');
}

async function triggerUpdateIndex(kbId) {
  try {
    const btn = dom.updateIndexBtn;
    if (btn) {
      btn.disabled = true;
      btn.innerText = '增量更新中...';
    }
    const res = await fetch(`/kb/${kbId}/update`, { method: 'POST' });
    const data = await res.json();
    if (!res.ok || data.success === false) throw new Error(data.detail || data.message || '更新失败');
    showToast('success', `增量完成，更新文件数: ${data.updated_files || 0}`);
    await loadFilesForKB(kbId);
  } catch (e) {
    showToast('danger', '增量失败: ' + e.message);
  } finally {
    if (dom.updateIndexBtn) {
      dom.updateIndexBtn.disabled = false;
      dom.updateIndexBtn.innerText = '增量更新';
    }
  }
}

// --- Config ---
const CONFIG_FIELDS = [
  { key: 'data_dir', label: '数据目录 (DATA_DIR)', ph: 'I:/SimpleRag/data' },
  { key: 'index_dir', label: '索引目录 (INDEX_DIR)', ph: 'I:/SimpleRag/storage' },
  { key: 'local_model_path', label: '本地模型路径 (LOCAL_MODEL_PATH)', ph: './models/qwen' },
  { key: 'ollama_model', label: 'Ollama 模型 (OLLAMA_MODEL)', ph: 'qwen3:0.6b' },
  { key: 'openai_base_url', label: 'OpenAI Base URL (OPENAI_BASE_URL)', ph: 'https://api.openai.com/v1' },
  { key: 'openai_chat_model', label: 'OpenAI Chat Model (OPENAI_CHAT_MODEL)', ph: 'gpt-4o-mini' },
  { key: 'openai_embedding_model', label: 'OpenAI Embedding (OPENAI_EMBEDDING_MODEL)', ph: 'text-embedding-3-small' },
  { key: 'api_key', label: 'API Key (OPENAI_API_KEY)', ph: 'sk-...' },
  { key: 'chunk_strategy', label: '分块策略 (CHUNK_STRATEGY)', type: 'select', options: ['semantic', 'fixed', 'recursive'] },
  { key: 'chunk_size', label: '分块大小 (CHUNK_SIZE)', type: 'number' },
  { key: 'chunk_overlap', label: '重叠大小 (CHUNK_OVERLAP)', type: 'number' },
  { key: 'min_chunk_size', label: '最小分块 (MIN_CHUNK_SIZE)', type: 'number' },
  { key: 'max_chunk_size', label: '最大分块 (MAX_CHUNK_SIZE)', type: 'number' },
  { key: 'semantic_chunk_threshold', label: '语义阈值 (SEMANTIC_CHUNK_THRESHOLD)', type: 'number', step: '0.01', helper: '范围 0-1，推荐 0.7' },
  { key: 'semantic_chunk_similarity_window_size', label: '相似度窗口 (SEMANTIC_CHUNK_SIMILARITY_WINDOW_SIZE)', type: 'number', helper: '语义分块的窗口大小' },
  { key: 'max_concurrent_chunking', label: '最大并发数 (MAX_CONCURRENT_CHUNKING)', type: 'number' },
];

async function initConfig() {
  try {
    const res = await fetch('/config');
    const data = await res.json();
    renderConfigForm(data);
  } catch (e) {
    console.error("Failed to load config", e);
    showToast('danger', '配置加载失败');
  }

  dom.saveConfigBtn.addEventListener('click', async () => {
    const formData = new FormData(dom.configForm);
    const payload = {};
    formData.forEach((val, key) => {
      if (val) payload[key] = isNaN(val) ? val : (val.includes('.') ? parseFloat(val) : parseInt(val));
    });

    // Handle empty strings
    CONFIG_FIELDS.forEach(f => {
        if (f.type === 'number' && !payload[f.key]) delete payload[f.key];
    });

    try {
      const btn = dom.saveConfigBtn;
      btn.innerText = '保存中...';
      btn.disabled = true;
      
      const res = await fetch('/config', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload)
      });
      const result = await res.json();
      showToast('success', result.message || '配置已保存');
    } catch (e) {
      showToast('danger', '保存失败: ' + e.message);
    } finally {
      dom.saveConfigBtn.innerText = '保存配置';
      dom.saveConfigBtn.disabled = false;
    }
  });
}

function renderConfigForm(data) {
  dom.configForm.innerHTML = CONFIG_FIELDS.map(field => {
    const val = data[field.key] !== undefined ? data[field.key] : '';
    let inputHtml = '';

    if (field.type === 'select') {
      const opts = field.options.map(o => `<option value="${o}" ${val === o ? 'selected' : ''}>${o}</option>`).join('');
      inputHtml = `<select name="${field.key}">${opts}</select>`;
    } else {
      const stepAttr = field.step ? ` step="${field.step}"` : '';
      inputHtml = `<input type="${field.type || 'text'}" name="${field.key}" value="${val}" placeholder="${field.ph || ''}"${stepAttr}>`;
    }

    const helper = field.helper ? `<div class="helper-text">${field.helper}</div>` : '';

    return `
      <div class="config-item">
        <label>${field.label}</label>
        ${inputHtml}
        ${helper}
      </div>
    `;
  }).join('');
}

// --- Chat Logic ---
function initChat() {
  // Auto-resize textarea
  dom.chatInput.addEventListener('input', function() {
    this.style.height = 'auto';
    this.style.height = (this.scrollHeight) + 'px';
    dom.sendBtn.disabled = !this.value.trim();
  });

  // Send on Shift+Enter
  dom.chatInput.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      sendMessage();
    }
  });

  dom.sendBtn.addEventListener('click', sendMessage);
  
  dom.clearBtn.addEventListener('click', () => {
    dom.chatHistory.innerHTML = `
      <div class="welcome-message">
        <h2>✨ 对话已清空</h2>
      </div>
    `;
  });
}

async function sendMessage() {
  const text = dom.chatInput.value.trim();
  if (!text || state.streaming) return;

  // User Message
  appendMessage('user', text);
  dom.chatInput.value = '';
  dom.chatInput.style.height = 'auto';
  dom.sendBtn.disabled = true;

  // Bot Placeholder
  const botMsgId = appendMessage('bot', '');
  const botMsgEl = document.getElementById(botMsgId);
  const contentEl = botMsgEl.querySelector('.content');
  // Create a container for markdown content specifically
  const markdownContainer = document.createElement('div');
  markdownContainer.className = 'markdown-content';
  contentEl.innerHTML = ''; // Clear initial content
  contentEl.appendChild(markdownContainer);
  
  state.streaming = true;
  state.controller = new AbortController();
  contentEl.dataset.streaming = "true";

  let fullText = "";
  
  try {
    const response = await fetch('/query_stream', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        query: text,
        mode: state.mode,
        show_sources: dom.showSources.checked,
        kb_id: state.kbId,
      }),
      signal: state.controller.signal
    });

    if (!response.ok) throw new Error(`HTTP error ${response.status}`);

    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let done = false;

    while (!done) {
      const { value, done: doneReading } = await reader.read();
      done = doneReading;
      const chunkValue = decoder.decode(value, { stream: !done });
      fullText += chunkValue;
      
      // Handle Sources Split
      const sourceMarker = '\n\n[SOURCES]\n';
      const splitIdx = fullText.indexOf(sourceMarker);
      let displayText = fullText;
      let sourcesData = null;

      if (splitIdx !== -1) {
        displayText = fullText.substring(0, splitIdx);
        // marker length = 12 chars: "\n\n[SOURCES]\n"
        const sourcesJson = fullText.substring(splitIdx + sourceMarker.length);
        try {
            // Only parse if it looks like complete JSON
            if (sourcesJson.trim().endsWith(']')) {
                sourcesData = JSON.parse(sourcesJson);
            }
        } catch (e) { /* waiting for more chunks */ }
      }

      // Update UI - Update markdown container specifically
      renderMarkdown(markdownContainer, displayText);
      
      if (sourcesData && splitIdx !== -1) {
        renderSources(contentEl, sourcesData);
      }
      
      scrollToBottom();
    }

  } catch (err) {
    if (err.name !== 'AbortError') {
      markdownContainer.innerHTML += `<br><span style="color:var(--danger)">[Error: ${err.message}]</span>`;
    }
  } finally {
    state.streaming = false;
    state.controller = null;
    contentEl.dataset.streaming = "false";
  }
}

function appendMessage(role, text) {
  const id = 'msg-' + Date.now();
  const div = document.createElement('div');
  div.className = `message ${role}`;
  div.id = id;
  
  // Ensure content structure allows separation of text and appended elements
  div.innerHTML = `
    <div class="avatar">${role === 'user' ? 'U' : 'AI'}</div>
    <div class="content">
        <div class="markdown-content markdown-body">${text}</div>
    </div>
  `;
  dom.chatHistory.appendChild(div);
  
  // If there is initial text, render it properly
  if (text) {
      const contentBody = div.querySelector('.markdown-content');
      renderMarkdown(contentBody, text);
  }
  
  scrollToBottom();
  return id;
}

function addSystemMessage(text) {
    const div = document.createElement('div');
    div.style.textAlign = 'center';
    div.style.color = 'var(--text-secondary)';
    div.style.fontSize = '0.8rem';
    div.style.margin = '10px 0';
    div.innerText = text;
    dom.chatHistory.appendChild(div);
    scrollToBottom();
}

function renderMarkdown(element, text) {
  if (window.marked) {
    element.innerHTML = window.marked.parse(text);
  } else {
    element.innerText = text;
  }
}

function renderSources(element, sources) {
  // Check if sources already rendered
  if (element.querySelector('.sources-list')) return;

  const sourcesHtml = sources.map(s => `
    <div class="source-item">
      <div class="source-meta">
        <span>#${s.index} 相关度: ${(s.score || 0).toFixed(2)}</span>
        <span title="${s.file_path}">${s.file_path ? s.file_path.split('/').pop() : '未知文件'}</span>
      </div>
      <div class="source-text">${s.text}</div>
    </div>
  `).join('');

  const container = document.createElement('div');
  container.className = 'sources-list';
  container.innerHTML = `<strong>参考来源:</strong>${sourcesHtml}`;
  element.appendChild(container);
}

function scrollToBottom() {
  dom.chatHistory.scrollTop = dom.chatHistory.scrollHeight;
}

// --- File Upload ---
function initUpload() {
  const { dropZone, fileInput, uploadStatus, rebuildBtn } = dom;

  dropZone.addEventListener('click', () => fileInput.click());
  
  dropZone.addEventListener('dragover', (e) => {
    e.preventDefault();
    dropZone.classList.add('dragover');
  });
  
  dropZone.addEventListener('dragleave', () => {
    dropZone.classList.remove('dragover');
  });

  dropZone.addEventListener('drop', (e) => {
    e.preventDefault();
    dropZone.classList.remove('dragover');
    handleFiles(e.dataTransfer.files);
  });

  fileInput.addEventListener('change', () => {
    handleFiles(fileInput.files);
  });

  async function handleFiles(files) {
    if (!files.length) return;
    
    const formData = new FormData();
    for (let f of files) formData.append('files', f);

    uploadStatus.innerHTML = `<span style="color:var(--accent)">正在上传 ${files.length} 个文件...</span>`;
    
    try {
      const res = await fetch(`/kb/${state.kbId}/upload`, { method: 'POST', body: formData });
      const data = await res.json();
      uploadStatus.innerHTML = `<span style="color:var(--success)">成功上传: ${data.saved.length} 个文件</span>`;
      await loadFilesForKB(state.kbId);
    } catch (e) {
      uploadStatus.innerHTML = `<span style="color:var(--danger)">上传失败: ${e.message}</span>`;
    }
  }

  rebuildBtn.addEventListener('click', async () => {
    if (!confirm('确定要重建当前知识库索引吗？这将清除现有索引并重新处理所有文档。')) return;
    
    rebuildBtn.disabled = true;
    rebuildBtn.innerHTML = '正在重建...';
    
    try {
      const res = await fetch(`/kb/${state.kbId}/rebuild`, {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({force: true, kb_id: state.kbId})
      });
      const data = await res.json();
      if(data.success) {
        showToast('success', '索引重建完成');
      } else {
        throw new Error(data.detail);
      }
      await loadFilesForKB(state.kbId);
    } catch (e) {
      showToast('danger', '重建失败: ' + e.message);
    } finally {
        rebuildBtn.disabled = false;
        rebuildBtn.innerHTML = `
            <svg viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2" width="16"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>
            重建向量索引
        `;
    }
  });
}
